package ex3.matchmaker;

import java.rmi.RemoteException;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
import ex3.Constants.*;
import java.util.Map;
import java.util.HashMap; // unsynchronized Hashmap is what we want
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * Implements the MatchMaker object.
 * @author rmn
 */
@SuppressWarnings("serial")
public class MatchmakerImpl extends UnicastRemoteObject implements Matchmaker {
	
	private int regPort;
	private final static int INITIAL_PORT = 3001, MAX_PORT = 30000;
	
	// class members:
	private Integer lastExecID;
	private Integer lastTaskQueueID;
	private Map<String, ExecutorData> executorsMap;
	private ReentrantReadWriteLock executorsMapLock;
	private Map<Character, ExecutorsContainer> capabilityQueues;
	private ReentrantReadWriteLock capabilityQueuesLock;
	
	/**
	 * The invocation of the matchmaker shall start here. Will create the repository and the
	 * matchmaker object.
	 * @param args command line arguments.
	 */
	public static void main(String[] args) {
		try { 
			@SuppressWarnings("unused")
			MatchmakerImpl  m = new MatchmakerImpl();
		} 
		catch (RemoteException e){
			System.out.print(".");
		}
			
		// TODO: maybe this thread can do kindof garbage collection (clean the irrelevant data from queues)
		// probably better not as this is too much of an overhead with little help.
	}
	
	/**
	 * Constructs the matchmaker object.
	 * @throws RemoteException
	 */
	public MatchmakerImpl() throws RemoteException {
		lastExecID = 0;
		lastTaskQueueID = 0;
		executorsMap = new HashMap<String, ExecutorData>();
		executorsMapLock = new ReentrantReadWriteLock();
		capabilityQueues = new HashMap<Character, ExecutorsContainer>();
		capabilityQueuesLock = new ReentrantReadWriteLock();
		
		regPort=createRegistry();
		System.out.println("Registry port is: " + regPort);
		ExecutorsContainer.setRepPort(regPort);
		try {
			Naming.rebind("//:"+regPort+"/matchmaker",this);
		}
		catch(Exception e) {
			System.out.println("Matchmaker failed to bind. Exiting.");	
			System.exit(1);
		}
	}
	
	/**
	 * This method creates a registry on localhost, and prints 
	 * the port that is used for it. Did not change this method :)
	 */
	private int createRegistry() throws RemoteException{
		RemoteException failureException=null;
		for(int i = INITIAL_PORT;i<=MAX_PORT;++i){
			try{
				LocateRegistry.createRegistry(i);
				System.out.println();
				return i;
			}catch(RemoteException e){
				failureException=e;
				System.out.print(".");
			}
		}
		System.err.println("Failed to create registry");
		throw failureException;
	}
	
	/**
	 * @see Matchmaker#assignExecutorID()
	 */
	public int assignExecutorID() throws RemoteException{
		synchronized(lastExecID) {
			return ++lastExecID;
		}
	}
	
	/**
	 * @see Matchmaker#assignTaskQueueID()
	 */
	public int assignTaskQueueID() throws RemoteException {
		synchronized(lastTaskQueueID) {
			return ++lastTaskQueueID;
		}
	}
	
	/**
	 * @see Matchmaker#updateExecutor(String, State, List)
	 */
	public void updateExecutor(String bindName, State state, List<Character> capabilities) throws RemoteException{
		ExecutorData exec = getExecutorData(bindName);
		
		exec.update(state, capabilities);
		if (state == State.IDLE)
			updateQueues(exec, capabilities);
	}

	/**
	 * Internal method to retrieve the ExecutorData object corresponding to the given bind
	 * name. If no such object exists, it will be created and inserted to the hash containing
	 * all such objects.
	 * @param bindName the bind name by which the executor is binded in the repository.
	 * @return the matching ExecutorData object.
	 */
	private ExecutorData getExecutorData (String bindName) {
		ExecutorData exec;
		executorsMapLock.readLock().lock();
		try {
			exec = executorsMap.get(bindName); // null if wasnt found
		} 
		finally { 
			executorsMapLock.readLock().unlock(); 
		}
		if (exec == null) {
			exec = new ExecutorData(bindName);
			executorsMapLock.writeLock().lock();
			try {
				executorsMap.put(bindName, exec);
			}
			finally {
				executorsMapLock.writeLock().unlock();
			}
		}
		return exec;
	}
	
	/**
	 * Once an executor calls update, this method is invoked to add it to the matching
	 * containers for all of its current capabilities.
	 * @param exec ExecutorData object.
	 * @param capabilities its updated capability list.
	 * @throws RemoteException
	 */
	private void updateQueues (ExecutorData exec, List<Character> capabilities) throws RemoteException {
		for (Character c : capabilities) {
			ExecutorsContainer queue = getQueue(c);
			synchronized(queue) {
				queue.addExecutor(exec);
			}
		}
	}
	
	/**
	 * This method returns the ExecutorsContainer of the capability asked.
	 * If no such container exists yet, one is created.
	 * @param c the capability of the container.
	 * @return the container corresponding to the capability.
	 */
	private ExecutorsContainer getQueue (Character c) {
		ExecutorsContainer queue;
		capabilityQueuesLock.readLock().lock();
		try {
			queue = capabilityQueues.get(c);
		}
		finally {
			capabilityQueuesLock.readLock().unlock();
		}
		if (queue == null) {
			queue = new ExecutorsContainer(c);
			capabilityQueuesLock.writeLock().lock();
			try {
				capabilityQueues.put(c, queue);
			}
			finally {
				capabilityQueuesLock.writeLock().unlock();
			}
		}
		return queue;
	}

	/**
	 * @see Matchmaker#getExecutor(Character, String)
	 */
	public String getExecutor(Character capability, String taskQueueBindName) {
		ExecutorsContainer queue = getQueue(capability);
		return queue.getExecutor(taskQueueBindName);
	}
	
}	